﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.IO.Ports;

namespace client
{
    public class CommandFailedException : Exception
    {
        private string mErrorInfo;
        public string ErrorInfo { get { return mErrorInfo; } }
        internal CommandFailedException(string info)
        {
            mErrorInfo = info;
        }
    };

    public class CommandTimeoutException : Exception
    {
    };

    internal enum CommandSuccess
    {
        CommandNotSent,
        CommandSent,
        CommandTimeout,
        CommandFail,
        CommandSuccess
    };

    internal class Command
    {
        internal Command()
        {
            Success = CommandSuccess.CommandNotSent;
        }

        public AutoResetEvent mDone = new AutoResetEvent(false);
        public string Request { get; set; }
        public string[] Result { get; set; }
        public int ResultLength { get; set; }
        public string Info { get; set; }
        public bool Question { get; set; }

        public long Sent { get; set; }
        public long Timeout { get; set; }
        public bool Silent { get; set; }

        public byte[] Data { get; set; }
        public int DataLength { get; set; }

        public CommandSuccess Success { get; set; }
    }

    public class Connection
    {
        private SerialPort mSerialPort = new SerialPort();
        private const long mDefaultTimeout = 2000;
        private string mParsed = string.Empty;
        private List<string> mParsedList = new List<string>();
        private int mParsedLength = 0;
        private Thread mSender = null;

        private List<Command> mCommands = new List<Command>();

        public Connection()
        {
            mSerialPort.DataReceived += new SerialDataReceivedEventHandler(OnDataReceived);
        }

        private void OnDataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            int size = 0;

            List<byte[]> bufferList = new List<byte[]>();

            try
            {
                lock (mSerialPort)
                {
                    int oldTimeOut = mSerialPort.ReadTimeout;
                    mSerialPort.ReadTimeout = 0;

                    while (mSerialPort.BytesToRead > 0)
                    {
                        byte[] buffer = new byte[1024];
                        size = mSerialPort.Read(buffer, 0, buffer.Length);
                        if (size > 0)
                        {
                            Array.Resize(ref buffer, size);
                            bufferList.Add(buffer);
                        }
                    }

                    mSerialPort.ReadTimeout = oldTimeOut;
                }
            }
            catch (Exception)
            {
            }

            foreach (byte[] buffer in bufferList)
            {
                OnMediaDataReceived(buffer, buffer.Length);
            }
        }

        public bool IsConnected
        {
            get { return mSerialPort.IsOpen; }
        }

        public bool SendCommand(string command)
        {
            return SendCommand(command, mDefaultTimeout);
        }

        public bool SendCommand(string command, long timeout)
        {
            return SendCommand(command, timeout, false);
        }

        public bool SendCommand(string command, long timeout, bool silent)
        {
            Command cmd = new Command();
            cmd.Request = command;
            cmd.Question = command.IndexOf('?') != -1;
            cmd.Sent = 0;
            cmd.Silent = silent;
            cmd.Timeout = timeout;

            lock (mCommands)
            {
                mCommands.Add(cmd);
                if (!IsConnected)
                {
                    mCommands.Remove(cmd);
                    return false;
                }
            }

            // Wait for the command to finish
            cmd.mDone.WaitOne();

            // Throw exception on timeout
            switch (cmd.Success)
            {
                case CommandSuccess.CommandFail:
                    throw new CommandFailedException(cmd.Info);
                case CommandSuccess.CommandTimeout:
                    throw new CommandTimeoutException();
                default:
                    throw new CommandFailedException(cmd.Info);

                // Command was successful
                case CommandSuccess.CommandSuccess:
                    return true;
            }
        }

        public string[] SendQuestion(string command)
        {
            return SendQuestion(command, mDefaultTimeout);
        }

        public string[] SendQuestion(string command, long timeout)
        {
            return SendQuestion(command, timeout, false);
        }

        public string[] SendQuestion(string command, long timeout, bool silent)
        {
            Command cmd = new Command();
            cmd.Request = command;
            cmd.Question = command.IndexOf('?') != -1;
            cmd.Sent = 0;
            cmd.Silent = silent;
            cmd.Timeout = timeout;

            lock (mCommands)
            {
                mCommands.Add(cmd);
                if (!IsConnected)
                {
                    mCommands.Remove(cmd);
                    return null;
                }
            }

            // Wait for the command to finish
            cmd.mDone.WaitOne();

            // Throw exception on timeout
            switch (cmd.Success)
            {
                case CommandSuccess.CommandFail:
                    throw new CommandFailedException(cmd.Info);
                case CommandSuccess.CommandTimeout:
                    throw new CommandTimeoutException();
                default:
                    throw new CommandFailedException(cmd.Info);

                // Command was successful
                case CommandSuccess.CommandSuccess:
                    return cmd.Result;
            }
        }

        private void OnMediaDataReceived(byte[] buffer, int length)
        {
            Command cmd = null;
            //Stopwatch sw = new Stopwatch();
            //sw.Start();

            lock (mCommands)
            {
                foreach (Command checkCmd in mCommands)
                {
                    if (checkCmd.Success == CommandSuccess.CommandSent)
                    {
                        cmd = checkCmd;
                        break;
                    }
                }
            }

            Parse(cmd, buffer, length);
        }

        private void Parse(Command cmd, byte[] buffer, int length)
        {
            string data = Encoding.ASCII.GetString(buffer, 0, length);

            lock (mParsed)
            {
                char[] tokens = data.ToCharArray();

                // Parse the incomming data
                foreach (char token in tokens)
                {
                    // Suppress control characters
                    if (token > (char)27)
                        mParsed += token;

                    // Carriage return or line feed - handle this reply
                    if (!((token == '\r' || token == '\n') && mParsed.Length > 0))
                        continue;

                    lock (cmd)
                    {
                        if (!cmd.Silent)
                            System.Diagnostics.Debug.WriteLine(String.Format("R<-[{0}]", mParsed));

                        if (cmd.Question && mParsed[0] != '#')
                        {
                            lock (mParsedList)
                                mParsedList.Add(mParsed);
                            mParsedLength += mParsed.Length;
                            mParsed = string.Empty;
                            continue;
                        }

                        if (mParsed[0] != '#')
                        {
                            lock (mParsedList)
                                mParsedList.Add(mParsed);
                            mParsedLength += mParsed.Length;
                        }

                        if (mParsed.StartsWith("#success"))
                        {
                            cmd.Success = CommandSuccess.CommandSuccess;
                            lock (mParsedList)
                            {
                                lock (mCommands)
                                    mCommands.Remove(cmd);

                                cmd.Result = mParsedList.ToArray();
                                cmd.ResultLength = mParsedLength;
                                cmd.mDone.Set();
                                mParsedList.Clear();
                            }
                        }
                        else if (mParsed.StartsWith("#fail"))
                        {
                            cmd.Success = CommandSuccess.CommandFail;
                            cmd.Info = mParsed.Substring(4);
                            lock (mParsedList)
                            {
                                lock (mCommands)
                                    mCommands.Remove(cmd);

                                cmd.Result = mParsedList.ToArray();
                                cmd.ResultLength = mParsedLength;
                                cmd.mDone.Set();
                                mParsedList.Clear();
                            }
                        }

                        mParsed = string.Empty;
                    }
                }
            }
        }

        private void Sender()
        {
            while (IsConnected)
            {
                Command cmd = null;
                lock (mCommands)
                {
                    Command workCmd = null;
                    Command sendCmd = null;

                    // Check if there are no commands beeing worked on
                    // if no command has been found send the command at the 
                    // end of the list (that has not already been sent of course)
                    foreach (Command checkCmd in mCommands)
                    {
                        if (checkCmd.Success == CommandSuccess.CommandNotSent)
                        {
                            sendCmd = checkCmd;
                        }
                        else if (checkCmd.Success == CommandSuccess.CommandSent)
                        {
                            workCmd = checkCmd;
                            break;
                        }
                    }

                    // Send the command if not already sending another command
                    if (workCmd == null && sendCmd != null)
                        cmd = sendCmd;
                }

                // Do we have a command to send?
                if (cmd != null)
                {
                    lock (cmd)
                    {
                        // Make sure the command has not already been answered
                        if (cmd.Success != CommandSuccess.CommandNotSent)
                            break;

                        mParsedLength = 0;

                        // Send the command 
                        lock (mSerialPort)
                        {
                            if (!cmd.Silent)
                                System.Diagnostics.Debug.WriteLine(String.Format("S->[{0}]", cmd.Request));

                            byte[] data = Encoding.ASCII.GetBytes(cmd.Request + "\r\n");
                            mSerialPort.Write(data, 0, data.Length);

                            cmd.Sent = DateTime.Now.Ticks;
                            cmd.Success = CommandSuccess.CommandSent;

                        }
                    }
                }

                // Check for timeouts
                lock (mCommands)
                {
                    for (int i = 0; i < mCommands.Count; i++)
                    {
                        Command checkCmd = mCommands[i];
                        if (new TimeSpan(DateTime.Now.Ticks - checkCmd.Sent).TotalMilliseconds < checkCmd.Timeout)
                            continue;

                        if (checkCmd.Success == CommandSuccess.CommandSent)
                        {
                            // Timeout detected
                            mCommands.Remove(checkCmd);
                            checkCmd.Success = CommandSuccess.CommandTimeout;
                            checkCmd.mDone.Set();

                            // Start over..
                            i = 0;
                        }
                    }
                }

                // To prevent 100% CPU usage.
                Thread.Sleep(1);
            }

            // Check if there are any commands waiting to be sent..
            lock (mCommands)
            {
                while (mCommands.Count != 0)
                {
                    Command cmd = mCommands[0];

                    mCommands.RemoveAt(0);
                    cmd.Success = CommandSuccess.CommandTimeout;
                    cmd.mDone.Set();
                }
            }
        }

        public bool Connect()
        {
            mSerialPort.PortName = "COM4";
            mSerialPort.BaudRate = 9600;
            mSerialPort.Open();

            // Start the handler
            mSender = new Thread(new ThreadStart(Sender));
            mSender.Start();

            while (true)
            {
                try
                {
                    SendCommand("hello");
                }
                catch
                {
                    continue;
                }
                break;
            }

            return true;
        }

        public void Close()
        {
            lock (mSerialPort)
            {
                if (mSerialPort.IsOpen)
                    mSerialPort.Close();
            }
        }
    }
}
